Skip to content

Inbox sidebar, browser auto-open, and eleven rounds of polish - #91

Merged
badcuban merged 19 commits into
mainfrom
feature/sidebar-polish
Jul 28, 2026
Merged

Inbox sidebar, browser auto-open, and eleven rounds of polish#91
badcuban merged 19 commits into
mainfrom
feature/sidebar-polish

Conversation

@badcuban

Copy link
Copy Markdown
Collaborator

Rebuilds the sidebar as an inbox and finishes the browser panel's agent
integration, over eleven review rounds against the live app.

The inbox sidebar

One flat list, held in creation order (pins float; activity never reorders a
row), folding past six quiet threads behind "Show 5 more". Finished work sits
in a collapsible "Wrapped" tail that keeps recognizable rows and the thread
you are standing in. Done is a resolved verdict, not stored state: blockers
(pending approval, running work, queued turns) outrank any mark, an override
older than the thread's latest activity is ignored (so threads file and
return by themselves), and unseen completions never auto-file. Projects
become a scope dropdown with needs-you counts; the rail, On Deck, and the
project tree are gone.

Rows are project-first with a strict layout collision rule (status always
fits; branch yields first). The hover card rides Base UI's PreviewCard with
one root per surface and payload-carrying triggers: open delay is paid once,
row-to-row swaps are instant, and the card stays while the pointer reads it.
Failed sessions gained a status pill; they previously rendered as healthy
idle.

Browser panel

Agent actions now auto-open a closed panel: the automation host moved from
inside BrowserPanel to a thread-level mount, with the webview registry and
agent pointer/activity in the panel store. A closed panel opens, waits up to
5s for the page, and proceeds — no new tool, so it works for provider
sessions with stale tool lists. Source control stops defaulting open.

Process artifacts

docs/design/polish-checklist.md accumulates every reviewed defect genre
(spacing, states, interaction, scroll, drawing, language, pixel-testing) and
was used as an executable audit twice, catching sticky headers, native-title
double popups, selectable chrome, non-tabular timers — and a browser test
file that asserted layout without ever loading the stylesheet.

Testing

vp fmt, lint, typecheck, 1937 web unit, 342 browser, full server suite green
on the merged tree. Screenshot review ran before every visual commit. One
known gap, flagged honestly: the auto-open path is covered by a fake-bridge
browser test but has not yet run on a live Electron window.

badcuban added 19 commits July 28, 2026 00:14
Studied from the competitor's Sidebar v2, which is a better piece of
thinking than its screenshot suggests. The parts worth taking are not the
cards: they are the sort discipline (creation order, held from open until
settled, so the screen only moves when a lifecycle does), the three-color
status budget, and unread as its own axis rather than a status.

Translated into rows and dividers at two lines apiece, with projects
demoted from tree to filter -- which is the change that retires the rail:
once every row carries its own project, nothing needs tracing back to a
group header. Settle becomes Done. Snooze deferred whole.

A sketch, not a build; open questions for Will are in the file.
Will answered the collapse question -- hidden entirely, same toggle as
today -- and could not answer the quick-links one because it was asked in
internal vocabulary. Rewritten the way the screen looks. The new choice on
the table is how project switching feels: a compact dropdown at two clicks
a switch, or a row of recent-project chips at one, each carrying its
needs-you count.
Chips replace the dropdown in the main frame, the second frame shows the
scoped state, and Done rows get their project labels back at readable
contrast. Will's objection to the competitor's settled tail was exact --
dimming every row to the same grey turns history into texture. The
section header carries done-ness; the rows keep their identity.

Add project lives in the trailing ellipsis chip, which is the project
menu: overflow projects, then the rare actions.
A folder-plus pinned to the right end of the chips row: always visible,
never scrolling away with the overflow, and unambiguous beside the compose
pencil because it wears the folder. The ellipsis chip goes back to meaning
only what it says -- projects that did not fit.
The lifecycle, the sorts, and the chips, as pure functions with the
components still to come.

Done is an override the user sets, never a state the thread has: it is
resolved against the thread's live condition on every read, and the
blockers outrank it in both directions. Work that is blocked on the user,
in flight, or sent-but-not-yet-adopted refuses to hide -- that last one
bounded by a grace window on both sides, because message timestamps
originate on whichever device sent them and a clock ahead of this one
would otherwise hold the state for the whole skew. New activity in a done
thread pulls it back to the live list by the same resolution, with nobody
having to un-mark anything. That is what makes Done safe to use freely.

The live list sorts by creation and pins alone; its input type does not
carry activity timestamps, so reordering it by activity is a compile
error, not a temptation. Done rows sort by when the work ended, label and
order from the same function so they cannot disagree.

Along the way: failed sessions get a pill. They had none -- an errored
thread showed the same nothing as a healthy idle one, and counted toward
nobody's attention.
The project tree becomes one flat list. Live threads hold creation order
-- pins float, nothing else moves a row -- with status carried in the row:
a three-color budget of amber for blocked-on-you, blue for working, red
for failed, and everything else resting unlabeled with a timestamp. Done
threads keep their names and their projects in a tail below, ordered by
when the work ended, because history you can still recognize is the point
of keeping it visible.

Projects stop being structure and become a scope: chips of the recently
active few, each wearing its needs-you count, the overflow behind an
ellipsis, add-project pinned at the row's end. Every row names its own
project, which is what finally retires the rail -- nothing needs tracing
back to a group header anymore.

Gone with it: the On Deck section (the live list is the deck now), the
destinations band, the sort and grouping menus that no longer governed
anything, project drag-ordering, the icon-collapse mode, and every helper
that existed only to serve them, down to the auto-animate dependency.
Sidebar.tsx drops from 3904 lines to 1390 while gaining a lifecycle.

Built by an agent from the settled sketch in docs/design/sidebar-inbox
.html and reviewed seam by seam; its judgment calls held, including two
this message should not bury: archive survives beside Done because hiding
forever and finished-but-kept are different verbs, and project management
moved to right-click on the chips when the headers that held it left.
Three complaints from the first live look, one lifecycle rule under two of
them.

The list held every thread ever opened, because Done was explicit-only.
Now a thread idle for two days files itself -- unless it holds a
completion the user has not seen, because filing unread work would be the
sidebar reading your mail. Underneath sits one rule doing the work of
three features: an override older than the thread's last activity is
ignored. New work outranks an old word in both directions, so a done
thread that starts again returns by itself and a reopened one that goes
quiet may leave again. Nobody un-marks anything.

The rows stop being uniform. A second line is spent, not given: status,
unseen completion, or a branch that says something -- and a branch named
main says nothing the row's existence does not. Quiet threads compress to
one line at the Done tail's density, dividers survive only where two tall
rows meet, and the hover actions get an opaque backing instead of
printing over titles.

The chips are gone after failing their first contact with a real sidebar
width: cramped, truncated, and no room for their counts. A quiet dropdown
scales instead, with every project, its needs-you count, and the rare
actions in one menu. Add project keeps its visible button; that decision
predates the chips and outlives them.

The browser fixtures now live ten minutes in the past instead of pinned
to March: a time-relative lifecycle files fixed-date threads as done, and
the suite caught exactly that -- after the unit tests, run first, did not.
Twice now in one night the gap between those suites has been where the
bugs were.
Round three of Will reviewing the real thing, and the round where the
review process itself got fixed: this commit was looked at as pixels
before it was committed, via a throwaway browser-suite capture, which is
what reviewing UI should have meant from the start.

The two-line rows return in full -- favicon, project, branch behind its
glyph, diffstat when the checkout is dirty, provider mark, the working
timer -- and the volume problem moves where it belongs: the live list
folds past six quiet threads behind "Show more", with rows that carry a
status exempt from the fold, because hiding a pending approval behind a
disclosure defeats the approval. The default-branch-hiding cleverness is
gone; a user who looks for the branch and finds nothing does not care
how principled its absence was.

The hard rule between every row is gone too. Space separates quiet rows;
the hairline survives only under section headers. The hover cluster now
hugs exactly its own buttons and composites the hovered row's colour
under itself, instead of laying an oversized slab across the title.

The hover card is back on every row, the project favicon rides ahead of
every project name everywhere one appears, and the sidebar's
auto-collapse is deleted outright -- Will closes his own sidebar.
Archive becomes something only a done thread can do. Live rows offer two
icon-only actions, pin and done, their words in tooltips; done rows offer
reopen and archive the same way. The lifecycle reads live, done,
archived, and the archive button stops needing a running-thread guard
because a running thread can no longer be where the button is.

Rows flip to project-first -- favicon, project, branch on the top line
with the status dot leading it, title on the second, both flush at the
row's edge. The gap Will asked about was the old indent; nothing indents
now. General Chats gets its own nav row under the search bar instead of
hiding in a menu footer. The scope row's vertical rhythm goes symmetric,
and every section divider takes the same inset as the Settings one he
pointed to as the cleaner look, reveal rows included.

Reviewed as pixels in the browser harness before committing, which is
the discipline round three bought.
The title tooltip goes; the hover card was already answering the same
question, and two popups for one hover is a defect however correct each
is alone. Section headers take the competitor's better idea: label and
count together -- Threads (39), Done (12) -- a hairline running out to
the right, and on Done a chevron that folds the section. Folded, it
still shows the thread you are standing in: collapsing your own location
out of existence feels broken without being a bug. The header's rule
replaces the divider above it; two lines a few pixels apart read as a
mistake.

The scope menu drops its checkmark for a quieter mark: every row is icon
and name, the selected one sits on a fill deliberately half the hover's
intensity -- the old tokens differed by one percent of white, which is
the same colour wearing two names. Each row grows a hover-visible
ellipsis opening the management menu right-click already opened, so the
menu's rare actions stop being a secret. The trigger stretches with the
sidebar, chevron riding the right edge, and General Chats gets the
breathing room the search row owed it.
The live list loses its header -- a label saying "Threads" over a list of
threads answered nothing -- and the settled one becomes "Tied off",
which is what you do to a thread when the work is finished, in an app
called Threadlines. The action follows: rows offer "Tie off", not
"Done". Internal names stay done*; this is display language.

The reveal row joins its section and goes incremental: five at a time,
with a search icon beside it and, once opened, a way to fold back. The
hover actions move into the timestamp's seat -- hovering trades the time
for the icons in place, on the row's own hover fill, the opaque backing
deleted entirely. On touch widths the icons sit beside the time instead,
because there is no hover to trade with.

General chats becomes a designed page: a reading column, date-grouped
rows, a real empty state. And the polish checklist lands in docs/design
-- every defect genre caught across six rounds of review, written as
checks the screenshot pass runs, because taste transfers as genres or
not at all.
The Tied off header becomes a single button -- label, rule and chevron
all toggle the fold, hover reads as a text shift, and it goes sticky on
an opaque ground so rows slide under it rather than through.

Live rows stop asking permission to know their branch: the git-status
gate that only fetched when a branch was recorded meant exactly the
threads missing one never asked, so every live row now watches its
checkout and shows the recorded branch or, failing that, where the
checkout actually is. The change-request lookup follows the same
resolved branch, so the row's PR badge and its branch label can no
longer disagree.

The checklist audit paid for itself on its first run, twice over. Once
by finding the sticky-header gap. Once by finding something better: the
hover card's browser tests never imported the stylesheet, so all four
layout assertions had been passing against unstyled DOM -- blind since
the day they were written. The file loads production styles now, the
truncation it claimed to test is actually tested, and the genre is
recorded in the checklist so the next blind test is found by grep
rather than by accident.
The settled section is Wrapped now, its action Wrap up -- Will's word,
and a better one than mine. The branch label walks back to recorded
branches only: six rows all chanting "main" was wallpaper, not
information, so the checkout's current ref moves to the hover card,
which had never shown a branch at all. The fetch stays loose underneath,
because the PR badge and diffstat it feeds were the part worth keeping.

The chats page centres its column and squares its radii to the family
the rest of the app's clickable rows use; the sidebar's General Chats
row grows a hover-revealed way to start one without the detour.

Then the audit: chrome rows sat at three different left insets against
the list's one, and now everything lands on the same edge. Decorative
title attributes were firing native popups over the hover card. Chrome
text was selectable. Time labels wiggled every tick for want of tabular
numerals. The empty sidebar named its problem without offering its
solution. All fixed, and two genres the checklist did not yet name are
recorded in it -- which is the whole arrangement: taste in, checks out.
A working row was printing "working - just now" past the sidebar's edge
while its branch truncated to garbage. Two causes, two fixes. The layout
gets a rule instead of a patch: the status slot always fits whole, and
the left side yields in fixed order -- branch vanishes entirely first
(half a branch name is worse than none), project truncates second, the
title never pays because it lives on its own line. And "just now" turns
out to be a how-long-ago phrase moonlighting as a duration:
ThreadElapsedLabel was borrowing formatElapsedDurationLabel, whose other
callers legitimately want words. Durations now have their own formatter
that only ever says 3s or 1m 5s.

The hover card tightens to the competitor's density without losing a
fact, gains the machine row we always had the data for, and annotates a
fallback branch as "- checkout" so ambient truth stops impersonating a
pinned one -- with a real space in the DOM text, because a margin is
invisible to a screen reader; the worktree annotation had the same
defect and got the same fix.

The search chrome hoists out of the scroll container -- one pinned
element, no sticky rivalry with the Wrapped header -- behind an opaque
ground and a short fade, no hairline. The General Chats row holds its
hover while its new-chat icon (now the chat-bubble the page uses) takes
its own. Pin hover joins the muted-to-foreground convention everyone
else follows.

Also measured, for the record: row content sits 12px from both edges.
The asymmetry Will sensed is the chrome fills' 8px inset against the
rows' full bleed, not the padding.
Will asked for verification of the subagent's claims, and the audit found
the report lying twice about its own work. "Pin hover now follows the
muted-to-foreground convention" -- except the pinned state carried
hover:text-primary, which in dark mode is DARKER than its resting colour;
the unpin you could not see was the fix's own regression. "The hover card
is tightened" -- padding and line-height were, but the width driving the
complaint was a fixed w-64 nobody touched. Now w-56, and rounded-md,
because the card was also the last rounded-lg in the sidebar's orbit --
a radius the round-eight audit normalized everywhere except the surface
it was auditing from.

The other six claims checked out exactly as reported: the hoisted search
chrome, the group-hover fill, the duration formatter, the collision
rule, the machine row, the screen-reader-real annotation spaces.

The lesson joins the process: a subagent's "fixed" is a claim like any
other, and the coordinator reads the line, not the summary.
A wrapped thread's detail is no less worth a hover than a live one's --
the card was simply never attached below the fold line. status null
reads as the idle state, and the card's activity timestamp does the
rest.
The thread hover card moves off the tooltip primitive onto the one built
for it. A tooltip names the thing under the pointer; a preview card is a
surface you can move onto and read -- and building the card on Tooltip
had put it in the same one-at-a-time group as the action buttons' own
tooltips, so hovering "Wrap up" evicted the card of the very row it sat
on, and leaving the trigger closed it before it could be read.

The shape underneath is one card per surface, not per row: each row is a
detached trigger carrying its thread as payload to its surface's single
root, so the open delay is paid once and neighbouring rows swap
instantly -- the old tooltip group's warmth, restored architecturally.
Per SURFACE matters: the sidebar and the chats page can be mounted at
once, and two roots sharing one handle is undefined behaviour, which the
card's own browser tests demonstrated as order-dependent flakes until
the handle moved into context.

Source control stops defaulting open on desktop: a third of the window
went to a panel most turns never touch. The toggle and the explicit
query param still open it.
A closed panel meant no automation host, and the agent dead-ended on
"the in-app browser connection isn't available" -- with no browser_open
tool able to help, because providers snapshot their tool list at session
start and a new tool is invisible to every open thread. So the existing
tools do what an assistant would: reaching for the browser opens it.

The host moves from inside the panel to beside the thread, connected
whenever a thread is open; the webview registry and the agent's
pointer/activity state move into the store the panel already reads. An
operation arriving with the panel closed opens it, waits up to five
seconds for the page to register -- a quarter of the broker's patience --
and proceeds; past the cap it falls back to the answer the host already
knew how to give. The registry stays a module-level map rather than
state: DOM nodes that churn on every tab mount, with nothing rendering
from them.

Unverified on a real Electron window; the browser test drives a fake
bridge. The live round trip is the one check left before this ships.
# Conflicts:
#	apps/web/package.json
#	pnpm-lock.yaml
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadlines Ready Ready Preview, Comment Jul 28, 2026 9:24am

Request Review

@github-actions github-actions Bot added size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 28, 2026
@badcuban
badcuban merged commit 7842e14 into main Jul 28, 2026
15 checks passed
@badcuban
badcuban deleted the feature/sidebar-polish branch July 28, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant